# Greg Johnston
#  <greg.johnston@gmail.com>
#  gtkview.py
#  The GTKView class to manipulate our Glade file

import sys, os
import gtk
import gtk.glade

from config import RitualConfig
from datetime import date
from calc import *

import matplotlib
matplotlib.use('GTK')
from matplotlib.figure import Figure
from matplotlib.axes import Subplot
from matplotlib.backends.backend_gtk import FigureCanvasGTK, NavigationToolbar

class GTKView:
	"""This is the GTK GUI controller for Ritual"""
	# Main
	def __init__(self):
		self.Config = RitualConfig()
		self.ViewerOffset = 0
		self.EditorShoesSoFar = []
		self.EditorCoursesSoFar = []

		self.gladefile = "ritual.glade"
		self.wTree = gtk.glade.XML(self.gladefile)

		self.Calendar = self.wTree.get_widget("calendar1")

		self.Saved = True

		# Set logo/icon images for windows
		for window in ["top", "editor", "about", "calculator"]:
			self.wTree.get_widget(window).set_icon_from_file(self.Config.config["logo"])
		self.wTree.get_widget("about").set_logo(self.wTree.get_widget("about").get_icon())

	def checksave(self):
		"""Checks to make sure you save when you're leaving a log"""
		if self.Saved == False:
			dialog = self.wTree.get_widget("checksave")
			response = dialog.run()
			if response == "quitsave": dialog.emit("on_imagemenuitem4_activate")
			elif response == "cancelquit": return False
			elif response == "quitnosave": pass
			else: print response

	def quit(self, error=0):
		"""Quits the progam as GTK needs it to, after making sure you save"""
		
		gtk.main_quit()
		sys.exit(error)

	def run(self):
		"""Shows the main window, sets up the View as we want it to start"""
		# Set up main window
		window = self.wTree.get_widget("top")
		window.connect("destroy", self.quit)
		self.calendar_to_month_year(date.today())
		gtk.main()

	def connect(self, dict):
		"""Connects a dictionary of functions to our signals"""
		self.wTree.signal_autoconnect(dict)

	def title(self, file, extra=""):
		"""Gives the window its title"""
		if file and file != "": t = os.path.split(file)[1] + extra + " - Ritual"
		else: t = "Ritual"
		self.wTree.get_widget("top").set_title(t)

	# Return Widget Values
	def get_value(self, widget):
		"""Returns the value of a widget, given its name"""
		if widget == "date": return self.wTree.get_widget("dateentry").get_text()
		elif widget == "course": return self.wTree.get_widget("courseentry-entry").get_text()
		elif widget == "distance": return self.wTree.get_widget("distanceentry").get_text()
		elif widget == "distanceunit": return self.wTree.get_widget("distanceunit").get_active()
		elif widget == "time": return self.wTree.get_widget("timeentry").get_text()
		elif widget == "shoes": return self.wTree.get_widget("shoesentry-entry").get_text()
		elif widget == "notes": return self.wTree.get_widget("notesentry").get_text()
		else: return None

	# I/O
	def fileprompt(self, type):
		"""Returns a filename; sets up a prompt call and calls it, given an action"""
		# Define our filters
		if type == "open" or type =="saveas":
			filter = gtk.FileFilter()
			filter.set_name("Ritual files (*.ritual)")
			filter.add_pattern("*.ritual")
		elif type == "convert":
			filter = gtk.FileFilter()
			filter.set_name("Coolrunning (downloadlog.cgi, *.csv)")
			filter.add_pattern("downloadlog.cgi")
			filter.add_pattern("*.csv")
		# Give values for boxes
		if type == "open":
			filename = self.prompt("Open...", gtk.STOCK_OPEN, filter,
									gtk.FILE_CHOOSER_ACTION_OPEN)
		elif type == "convert":
			filename = self.prompt("Convert...", gtk.STOCK_CONVERT,
									filter, gtk.FILE_CHOOSER_ACTION_OPEN)
		elif type == "saveas":
			filename = self.prompt("Save as...", gtk.STOCK_SAVE, filter,
									gtk.FILE_CHOOSER_ACTION_SAVE)
		return filename


	def prompt(self, title, okaybutton, filter, action):
		"""Returns a filename, given a title, filter, and GTK stock button"""
		dialog = gtk.FileChooserDialog(title=title,action=action,
										buttons=(gtk.STOCK_CANCEL,gtk.RESPONSE_CANCEL,
												 okaybutton,gtk.RESPONSE_OK))
		dialog.set_default_response(gtk.RESPONSE_OK)
		dialog.add_filter(filter)
		dialog.set_current_folder(os.path.expanduser("~"))

		response = dialog.run()
		if response == gtk.RESPONSE_OK:
			filename = dialog.get_filename()
		elif response == gtk.RESPONSE_CANCEL:
			filename = True
		dialog.destroy()
		return filename

	def set_saved(self, status, file):
		"""Sets the saved-status to True or False"""
		if status == True:
			self.title(file)
			self.Saved = True
		else:
			self.title(file, " *")
			self.Saved = False

	# Calendar
	def clear_calendar(self):
		"""Clears the marked days on the calendar"""
		self.Calendar.clear_marks()

	def calendar_mark_month(self, log):
		"""Marks all the days in the selected month which have runs in the passed log"""
		self.Calendar.clear_marks()
		chosen_month = self.Calendar.get_date()[1]+1 # returns month as 0-11
		chosen_year = self.Calendar.get_date()[0]
		for date_tuple in log.log.keys():
			date = date_tuple[0]
			if date.month == chosen_month and date.year == chosen_year:
				self.Calendar.mark_day(date.day)

	def calendar_to_month_year(self, date):
		"""Moves the calendar to the month and year it is passed"""
		self.Calendar.year = date.year
		self.Calendar.month = date.month

	def calendar_selected_date_string(self):
		"""Returns the date selected by the calendar as a MM/DD/YYYY string"""
		tuple = self.Calendar.get_date()
		# make sure we add 1 to the month is because it gives months 0-11
		return str(tuple[1]+1) + "/" + str(tuple[2]) + "/" + str(tuple[0])

	def calendar_selected_date(self):
		"""Returns the date selected by the calendar as a MM/DD/YYYY string"""
		tuple = self.Calendar.get_date() # returns date as (year, month, day)
		# make sure we add 1 to the month is because it gives months 0-11
		return date(tuple[0], tuple[1]+1, tuple[2]) # we want to make (year, month, day) 

	# Editor
	def show_editor(self, run):
		"""Shows the Editor window"""
		self.wTree.get_widget("dateentry").set_text(self.calendar_selected_date_string())
		self.wTree.get_widget("courseentry-entry").set_text(run.course)
		self.wTree.get_widget("distanceentry").set_text(str(run.distance))
		self.wTree.get_widget("distanceunit").set_active(run.unit)
		self.wTree.get_widget("timeentry").set_text(seconds_timestr(run.time))
		self.wTree.get_widget("notesentry").set_text(run.notes)
		self.update_pace()
		self.wTree.get_widget("editor").show()

	def date_today(self, widget):
		"""Sets the value of the date field to today's date"""
		self.wTree.get_widget("dateentry").set_text(date.today().strftime("%m/%d/%Y"))

	def update_pace(self, widget=None, signal=None):
		"""Updates the pace label value to read the pace, based on distance/time"""
		tstr = self.wTree.get_widget("timeentry").get_text()
		unit = self.wTree.get_widget("distanceunit").get_active()
		try:
			distance = float(self.wTree.get_widget("distanceentry").get_text())
			return self.wTree.get_widget("pacevalue").set_text(calc_pace(tstr, distance, unit))
		except:
			return 0

	def tree2list(self, model):
		"""Extracts a list from a GTK.TreeModel"""
		lst = []
		iter = model.get_iter_first()
		while (iter != None):
			lst.append(model.get_value(iter, 0))
			iter = model.iter_next(iter)
		return lst

	def set_combos(self, log):
		"""Sets up the two ComboBoxEntry's, given a log"""
		if self.EditorCoursesSoFar == [] and self.EditorShoesSoFar == []:
			self.wTree.get_widget("courseentry").remove_text(0) # remove our primers
			self.wTree.get_widget("shoesentry").remove_text(0)

		self.EditorCoursesSoFar = self.tree2list(self.wTree.get_widget("courseentry").get_model())
		self.EditorShoesSoFar = self.tree2list(self.wTree.get_widget("shoesentry").get_model())

		if len(log) == 0: pass
		else:
			self.set_courses_combo(log.courses)
			self.set_shoes_combo(log)

	def set_courses_combo(self, courses):
		"""Sets up the courseentry ComboBoxEntry"""
		combobox = self.wTree.get_widget("courseentry")
		for course in sorted(courses.keys()):
			if not course in self.EditorCoursesSoFar:
				self.EditorCoursesSoFar += [course]
		self.set_model_from_list(combobox, self.EditorCoursesSoFar)

	def set_shoes_combo(self, log):
		"""Sets up the shoesentry ComboBoxEntry"""
		combobox = self.wTree.get_widget("shoesentry")
		for date in log.log:
			entry = log.log[date]
			if not entry.shoes in self.EditorShoesSoFar:
				self.EditorShoesSoFar += [entry.shoes]
		self.EditorShoesSoFar = sorted(self.EditorShoesSoFar)
		self.set_model_from_list(combobox, self.EditorShoesSoFar)

	def set_model_from_list (self, cb, items):
		"""Setup a ComboBox or ComboBoxEntry based on a list of strings."""
		# Function by Thomas Hinkle, found at http://eccentric.cx/misc/pygtk/pygtkfaq.html#16.8          
		model = gtk.ListStore(str)
		for i in items:
			model.append([i])
		cb.set_model(model)
		if type(cb) == gtk.ComboBoxEntry:
			cb.set_text_column(0)
		elif type(cb) == gtk.ComboBox:
			cell = gtk.CellRendererText()
			cb.pack_start(cell, True)
			cb.add_attribute(cell, 'text', 0)

	def set_distance_from_course(self, courses):
		"""Sets the distance and unit entries based on the course entry"""
		course = self.wTree.get_widget("courseentry-entry").get_text()
		if course in courses:
			self.wTree.get_widget("distanceentry").set_text(courses[course][0])
			print courses[course]
			self.wTree.get_widget("distanceunit").set_active(courses[course][1])

	def hide_editor(self, widget=None):
		"""Hides the Editor dialog"""
		self.wTree.get_widget("editor").hide()

	# Calculator
	def calc_add(self, button):
		"""Adds a row to the calculator"""
		vbox = button.get_parent().get_parent()
		hbox = gtk.HBox()
		vbox.add(hbox)
		entry = gtk.Entry()
		hbox.add(entry)
		entry.grab_focus()
		hbox.show()
		entry.show()

	def time_calc(self, widget):
		"""Returns a total time after popping up a calculator"""
		dialog = self.wTree.get_widget("calculator")
		response = dialog.run()
		times = []
		if response == 1: # OK
			for hbox in self.wTree.get_widget("dialog-vbox4").get_children():
				for child in hbox.get_children():
					try:
						times += [child.get_text()]
					except:
						pass
		elif response == 0: # Cancel
			return "HH:MM:SS"
		final = add_times(times)
		dialog.hide()
		return final

	def time_calc_set(self, widget):
		"""Sets the timeentry to the calculator's return"""
		self.wTree.get_widget("timeentry").set_text(self.time_calc(widget))

	# Curve
	def mpw_curve_points(self, log):
		"""Generates a list of points for the Curve widget to display"""
		vector = range(1, 53)
		for week in range(1, 53):
			vector[week-1] = log.miles_in_week(week, self.Calendar.get_date()[0])
		return stripzeros(vector)

	def show_curve(self, log):
		"""Shows the Curve widget, updated"""
		graphview = self.wTree.get_widget("vbox3")
		for child in graphview.get_children(): # remove any old graphs
			child.destroy()
		self.Graph = Figure(figsize=(3, 3), dpi=72)
		ax = self.Graph.add_subplot(111)
		ax.set_title('Miles per Week')
		ax.grid(True)
		list = self.mpw_curve_points(log)
		ax.scatter(range(1, len(list)+1), list)
		ax.plot(range(1, len(list)+1), list)
		ax.axis([1, len(list)+1, min(list), max(list)+5])
		canvas = FigureCanvasGTK(self.Graph) # a gtk.DrawingArea
		canvas.show()
		graphview.pack_start(canvas, True, True)


	# About
	def show_about(self, widget):
		"""Runs the AboutDialog widget"""
		self.wTree.get_widget("about").run()
		self.wTree.get_widget("about").hide()

	## Viewer controllers
	def reader_move(self, offset, log):
		"""Moves the reader forwards offset runs"""
		self.ViewerOffset += offset
		self.show_run_data(self.Calendar, log)

	def show_today_run_data(self, log):
		"""Updates the right-hand viewer to the current day"""
		self.show_run_data(self.Calendar, log, date.today())

	# show_run_data the EVIL!
	def show_run_data(self, calendar, log, passdate=None):
		"""Updates the right-hand viewer to the run passed"""
		if passdate:
			searchdate = passdate
		else:
			selected = calendar.get_date()
			searchdate = date(selected[0], selected[1]+1, selected[2]) #(year, month+1, day)
		# Get a list of the runs on the selected day
		results = log.runs_on(searchdate)
		# Test number of results we got
		if len(results) == 0:
			self.wTree.get_widget("table2").hide()
			self.wTree.get_widget("noruns").show()
			return 0 # nothing to see!
		# If there's more than one, be ready to deal with that with arrows
		elif len(results) > 1:
			self.wTree.get_widget("previous").show()
			self.wTree.get_widget("next").show()
			self.wTree.get_widget("noruns").hide()
			self.wTree.get_widget("table2").show()
		else:
			self.wTree.get_widget("previous").hide() # just in case they're being
			self.wTree.get_widget("next").hide() #  shown already
			self.wTree.get_widget("noruns").hide()
			self.wTree.get_widget("table2").show()
		if len(results) > 0:
			# Show Edit/Delete
			self.wTree.get_widget("hbox2").show()
			self.wTree.get_widget("edit").show()
			self.wTree.get_widget("delete").show()
		# Choose the run to display
		if self.ViewerOffset < 0:
			self.ViewerOffset = len(results)-1
		elif self.ViewerOffset > len(results)-1:
			self.ViewerOffset = 0
		# Actually show the stuff
		self.wTree.get_widget("table2").show()
		run = log.get_run(searchdate, self.ViewerOffset)
		dist = log.entry_distance(run)
		self.wTree.get_widget("datampw").set_text(str(log.mpw(searchdate)))
		self.wTree.get_widget("datadate").set_text(date_string(searchdate))
		self.wTree.get_widget("datacourse").set_text(unescape(run.course))
		self.wTree.get_widget("datadistance").set_text(str(dist[0]) + " " + code_unit(dist[1]))
		self.wTree.get_widget("datatime").set_text(seconds_timestr(run.time))
		self.wTree.get_widget("datapace").set_text(calc_pace(seconds_timestr(run.time), run.distance, run.unit))
		shoestring = unescape(run.shoes) + " (" + str(log.miles_on_shoe(run.shoes)) + " miles)"
		self.wTree.get_widget("datashoes").set_text(shoestring)
		self.wTree.get_widget("datanotes").set_text(unescape(run.notes))
		self.show_curve(log)

if __name__ == "__main__":
	view = GTKView()
	view.set_courses_combo({"Bacon": 2.5, "Mahoney's": 4, "Bacon": 2.5})
	view.wTree.get_widget("editor").show()
	view.wTree.get_widget("calculator").show()
	view.run()
